using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Linq;
using System.Reflection;
using CoreEx.Common.Extensions;
using CoreEx.Database.Metadata;
using CoreEx.Database.Query.Expressions;
using CoreEx.DynamicInvoker;
using LinFu.IoC;
using LinFu.IoC.Configuration;
using LinFu.IoC.Interfaces;

namespace CoreEx.Database.Linq.Translators
{
    /// <summary>
    /// Provides a base class for implementing the <see cref="IExpressionTranslator_generic{TExpression}"/> interface.
    /// </summary>
    /// <typeparam name="TExpression">The type of <see cref="Expression"/> to be translated.</typeparam>
    public abstract class ExpressionTranslatorGenericBase<TExpression> :IInitialize, IExpressionTranslator_generic<TExpression> where TExpression:Expression
    {
        
        private IAliasProvider _aliasProvider;        
        private ITypeInfoMapper _typeInfoMapper;

        private readonly IDictionary<Type, object> _translators = new Dictionary<Type, object>();

        private static readonly IDictionary<Type,MethodInfo> _translateMethods = new Dictionary<Type, MethodInfo>(); 

        /// <summary>
        /// Gets the <see cref="IServiceContainer"/> that
        /// created this instance.
        /// </summary>
        protected IServiceContainer ServiceContainer { get; private set; }

        

        /// <summary>
        /// Gets an <see cref="IAliasProvider"/> instance that is responsible 
        /// for providing table aliases.
        /// </summary>
        protected IAliasProvider AliasProvider
        {
            get {return GetAliasProvider();}            
        }

        private IAliasProvider GetAliasProvider()
        {
            if (_aliasProvider == null)
                _aliasProvider = ServiceContainer.GetService<IAliasProvider>();
            return _aliasProvider;
        }

        /// <summary>
        /// Gets an <see cref="ITypeInfoMapper"/> instance that provides
        /// the mapping between types and database tables.
        /// </summary>
        protected ITypeInfoMapper TypeInfoMapper
        {
            get {return GetTypeInfoMapper();}            
        }

        private ITypeInfoMapper GetTypeInfoMapper()
        {
            if (_typeInfoMapper == null)
                _typeInfoMapper = ServiceContainer.GetService<ITypeInfoMapper>(string.Empty,TranslateResult.ConnectionName);
            return _typeInfoMapper;
        }


        /// <summary>
        /// Gets the <see cref="DbExpressionFactory"/> that is used to create 
        /// new <see cref="DbExpression"/> instances.
        /// </summary>
        protected static DbExpressionFactory ExpressionFactory { get; private set; }

        /// <summary>
        /// Initializes the <see cref="ExpressionTranslatorGenericBase{TExpression}"/> class.
        /// </summary>        
        static  ExpressionTranslatorGenericBase()
        {
            ExpressionFactory = new DbExpressionFactory();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ExpressionTranslatorGenericBase{TExpression}"/> class.
        /// </summary>
        /// <param name="translateResult">The <see cref="TranslateResult"/> that 
        /// provides aggregated information about the current query translation process.</param>
        protected ExpressionTranslatorGenericBase(TranslateResult translateResult)
        {
            TranslateResult = translateResult;
            ExpressionFactory = new DbExpressionFactory();
        }

        /// <summary>
        /// Gets the <see cref="TranslateResult"/> instance containing 
        /// detail information about the current query translation process.
        /// </summary>
        protected TranslateResult TranslateResult { get; private set;}


        /// <summary>
        /// Translates the <paramref name="expression"/> into a <see cref="DbExpression"/> instance.
        /// </summary>
        /// <param name="expression">The <see cref="Expression"/> to translate.</param>
        /// <returns><see cref="DbExpression"/></returns>
        public abstract DbExpression Translate(TExpression expression);

        public DbExpression TranslateAsSubQuery(MethodCallExpression methodCallExpression, MappedProperty outerMappedProperty)
        {
            return null;   
        }

        

        /// <summary>
        /// Visits the <paramref name="expression"/> and resolves the actual 
        /// <see cref="IExpressionTranslator_generic{TExpression}"/> to be used based on the expression type.
        /// </summary>
        /// <param name="expression">The <see cref="Expression"/> to translate.</param>
        /// <returns><see cref="DbExpression"/></returns>
        protected DbExpression Visit(Expression expression)
        {
            if (expression.NodeType == ExpressionType.Lambda)
                return Visit(((LambdaExpression) expression).Body);
            
            
            var translator = GetActualExpressionTranslator(expression);
            var dbExpression = ExecuteTranslateMethod(translator, expression);
            Trace.WriteLine(string.Format("Exresssion {0} translated to {1}", expression, dbExpression));
            return dbExpression;
        }



        private static DbExpression ExecuteTranslateMethod(object targetTranslator, Expression expression)
        {
            var method = GetTranslateMethod(targetTranslator.GetType());
            var dbExpression = (DbExpression)method.DynamicInvoke(targetTranslator,expression);
            return dbExpression;
        }

        private static MethodInfo GetTranslateMethod(Type type)
        {
            lock (_translateMethods)
            {
                if (!_translateMethods.ContainsKey(type))
                {
                    var translateMethod = type.GetMethod("Translate");
                    _translateMethods.Add(type, translateMethod);
                }
                return _translateMethods[type];
            }
        }



        private object GetActualExpressionTranslator(Expression expression)
        {
            var expressionType = expression.GetType();            
            if (!_translators.ContainsKey(expressionType))
            {
                object translator = CreateTranslatorInstance(expressionType);
                _translators.Add(expressionType,translator);
            }
            return _translators[expressionType];
        }


        private object CreateTranslatorInstance(Type expressionType)
        {
            var translatorType = typeof (IExpressionTranslator_generic<>).MakeGenericType(expressionType);
            try
            {
                return ServiceContainer.GetService(translatorType,
                                                    TranslateResult);
            }
            catch(ServiceNotFoundException serviceNotFoundException)
            {
                throw new NotSupportedException(string.Format("Expression type : {0} is not supported",
                                                              expressionType.Name), serviceNotFoundException);
            }
        }



        protected DbExpression CreateQuery(MemberInfo member)
        {
            TableSource tableSource = CreateTableSource(member);

            var tableExpression = tableSource.TableExpression;
            var projectionExpression = tableSource.Projection.DbExpression;

            return DbQueryFactory.Select(projectionExpression)
                .From(tableExpression);
        }


        private Projection CreateProjectionFromTableSource(TableSource tableSource)
        {
            var mappedProperties = tableSource.MappedProperties.Select(pm => pm.Value.PropertyMappingInfo);
            var projection = new Projection(tableSource);
            foreach (var mappedProperty in mappedProperties.Where( p => p.RelationType == CardinalityType.None))
            {
                var columnName = mappedProperty.ColumnInfo.ColumnName;
                var columnAlias = TranslateResult.GetUniqueColumnName(columnName);
                var columnExpression = CreateAliasedColumnExpression(columnName, columnAlias, tableSource.Alias);
                var projectedColumn = new ProjectedColumn
                {
                    TargetProperty = mappedProperty.Property,
                    ColumnName = columnAlias,
                    DbExpression = columnExpression
                };
                projection.ProjectedColumns.Add(projectedColumn);
            }
            return projection;
        }


        private static DbExpression CreateAliasedColumnExpression(string columnName, string columnAlias, string columnPrefix)
        {
            var columnExpression = ExpressionFactory.Column(columnPrefix, columnName);
            if (columnAlias != columnName)
                return ExpressionFactory.Alias(columnExpression, columnAlias);
            return columnExpression;
        }


        private TableSource CreateTableSource(MemberInfo memberInfo)
        {
            var typeMappingInfo = TypeInfoMapper.MapFrom(memberInfo.GetMemberType());
            var alias = AliasProvider.GetAliasFor(memberInfo);
            var tableSource = new TableSource(typeMappingInfo)
            {
                TargetMember = memberInfo,
                Alias = alias,
                TableExpression =
                ExpressionFactory.Table(typeMappingInfo.TableInfo.TableName, alias)
            };
            Projection projection = CreateProjectionFromTableSource(tableSource);
            tableSource.Projection = projection;
            TranslateResult.TableSources.Add(memberInfo, tableSource);
            return tableSource;
        }


        /// <summary>
        /// Returns the <see cref="TableSource"/> that represents a table in the final query 
        /// that is needed to navigate the target member expression.
        /// </summary>
        /// <param name="targetMemberExpression">The <see cref="MemberExpression"/> for which to return a <see cref="TableSource"/> instance.</param>
        /// <remarks>
        /// This method will create the necessary join expressions that is needed to access the database column 
        /// that the <paramref name="targetMemberExpression"/> represents.
        /// </remarks>
        /// <returns><see cref="TableSource"/></returns>
        protected TableSource GetTableSource(MemberExpression targetMemberExpression)
        {                        
            Expression relationalExpression = GetRelationalExpression(targetMemberExpression);

            var targetMember = GetMemberInfo(relationalExpression);
            
            if (IsJoined(relationalExpression))
                return TranslateResult.TableSources[targetMember];
            
            return CreateJoinedTableSource(relationalExpression);            
        }


        private Expression GetRelationalExpression(MemberExpression memberExpression)
        {
                                                
            Expression relationalExpression;
            if (MemberRepresentsRelation(memberExpression.Member))
                relationalExpression = memberExpression;
            else
                relationalExpression = memberExpression.Expression;
            return relationalExpression;
        }


        private TableSource CreateJoinedTableSource(Expression targetExpression)
        {
            var joinSequence = GetJoinSequence(targetExpression); 
            foreach (var expression in joinSequence)
                EnsureMemberIsJoined(expression);            
            return TranslateResult.TableSources[GetMemberInfo(targetExpression)];
        }
       
        private bool IsJoined(Expression expression)
        {
            var memberInfo = GetMemberInfo(expression);            
            return TranslateResult.TableSources.ContainsKey(memberInfo);                
        }

        private void EnsureMemberIsJoined(Expression targetExpression)
        {
            if (!IsJoined(targetExpression))            
                CreateJoinsNeededToAccessMember((MemberExpression)targetExpression);
        }




        private void CreateJoinsNeededToAccessMember(MemberExpression memberExpression)
        {
            string alias = AliasProvider.GetAliasFor(memberExpression.Member);
            MemberInfo outerMemberInfo = GetMemberInfo(memberExpression.Expression);
            TableSource outerTableSource = TranslateResult.TableSources[outerMemberInfo];
           
            DbExpression joinExpression = CreateJoinExpression(memberExpression, outerTableSource, alias);


            var memberInfo = memberExpression.Member;

            var typeMappingInfo = TypeInfoMapper.MapFrom(memberInfo.GetMemberType());

            var tableSource = new TableSource(typeMappingInfo);
            tableSource.Alias = alias;
            tableSource.TargetMember = memberInfo;
            tableSource.TableExpression = joinExpression;
            tableSource.OuterTableSource = outerTableSource;
            
            outerTableSource.JoinedTableSources.Add(tableSource);
            TranslateResult.TableSources.Add(memberInfo,tableSource);            
        }

        private DbExpression CreateJoinExpression(MemberExpression memberExpression, TableSource outerTableSource, string alias)
        {
            var outerAlias = outerTableSource.Alias;
            var outerPropertyMappingInfo = outerTableSource.MappedProperties[(PropertyInfo)memberExpression.Member].PropertyMappingInfo;
            var foreignKeyInfo = outerPropertyMappingInfo.ForeignKeyInfo;           
            var cardinality = outerPropertyMappingInfo.RelationType;

            DbExpression joinCriteraExpression = null;
            foreach (var foreignKeyColumnInfo in foreignKeyInfo.Columns)
            {
                DbExpression equalsExpression = null;

                if (cardinality == CardinalityType.OneToOne)
                {
                    equalsExpression = ExpressionFactory.Column(outerAlias,foreignKeyColumnInfo.PrimaryColumnInfo.ColumnName) ==
                                       ExpressionFactory.Column(alias, foreignKeyColumnInfo.ForeignColumnInfo.ColumnName);
                }

                if (cardinality == CardinalityType.ManyToOne)
                {
                    equalsExpression =
                        ExpressionFactory.Column(outerAlias, foreignKeyColumnInfo.ForeignColumnInfo.ColumnName) ==
                        ExpressionFactory.Column(alias, foreignKeyColumnInfo.PrimaryColumnInfo.ColumnName);

                }

                if (joinCriteraExpression.IsNull())
                    joinCriteraExpression = equalsExpression;
                else
                    joinCriteraExpression = joinCriteraExpression & equalsExpression;


            }


            DbExpression joinExpression;
            if (cardinality == CardinalityType.OneToOne)            
                joinExpression = ExpressionFactory.MakeJoin(DbJoinExpressionType.LeftOuterJoin,
                                                            ExpressionFactory.Table(foreignKeyInfo.Table.TableName, alias),
                                                            joinCriteraExpression);
            else
            {
                joinExpression = ExpressionFactory.MakeJoin(DbJoinExpressionType.LeftOuterJoin,
                                                            ExpressionFactory.Table(
                                                                foreignKeyInfo.PrimaryTable.TableName, alias),
                                                            joinCriteraExpression);
            }
            
            return joinExpression;
        }


        private static IEnumerable<Expression> GetJoinSequence(Expression expression)
        {
            IList<Expression> joinSequence = new List<Expression>();
            while (expression.NodeType != ExpressionType.Parameter)
            {
                joinSequence.Add(expression);
                expression = ((MemberExpression) expression).Expression;
            }            
            return joinSequence.Reverse().ToList();
        }
 

        private static MemberInfo GetMemberInfo(Expression expression)
        {
            if (expression.NodeType == ExpressionType.MemberAccess)
                return ((MemberExpression) expression).Member;
            return expression.Type;
        }



        protected bool MemberRepresentsRelation(MemberInfo memberInfo)
        {
            var type = memberInfo.GetMemberType();
            //var elementType = type.GetGenericElementType();
            return !type.IsSystemType() && !type.IsEnumerable();
        }


        /// <summary>
        /// Initializes the current service instance.
        /// </summary>
        /// <param name="source">The <see cref="IServiceContainer"/> that created this service instance.</param>
        public void Initialize(IServiceContainer source)
        {
            ServiceContainer = source;
        }

        
    }
}